// ResearchPool.cs
//
// Copyright (c) 2007 Mike Strobel
//
// This source code is subject to the terms of the Microsoft Reciprocal License (Ms-RL).
// For details, see <http://www.opensource.org/licenses/ms-rl.html>.
//
// All other rights reserved.

using System;
using System.Collections.Generic;
using System.ComponentModel;

using Supremacy.Annotations;
using Supremacy.Entities;
using Supremacy.Game;
using Supremacy.Tech;
using Supremacy.Types;
using Supremacy.Utility;

using Wintellect.PowerCollections;

using System.Linq;

namespace Supremacy.Economy
{
    /// <summary>
    /// Represents a research project being undertaken by a civilization in the game.
    /// </summary>
    [Serializable]
    public class ResearchProject
    {
        private readonly int _applicationId;
        private readonly Meter _progress;

        /// <summary>
        /// Gets the progress of a <see cref="ResearchProject"/>.
        /// </summary>
        /// <value>The progress.</value>
        public Meter Progress
        {
            get { return _progress; }
        }

        /// <summary>
        /// Gets a value indicating whether this <see cref="ResearchProject"/> is finished.
        /// </summary>
        /// <value>
        /// <c>true</c> if this <see cref="ResearchProject"/> is finished; otherwise, <c>false</c>.
        /// </value>
        public bool IsFinished
        {
            get { return _progress.IsMaximized; }
        }

        /// <summary>
        /// Gets the application being researched in this <see cref="ResearchProject"/>.
        /// </summary>
        /// <value>The application.</value>
        public ResearchApplication Application
        {
            get { return GameContext.Current.ResearchMatrix.GetApplication(_applicationId); }
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="ResearchProject"/> class.
        /// </summary>
        /// <param name="application">The application.</param>
        public ResearchProject(ResearchApplication application)
        {
            if (application == null)
                throw new ArgumentNullException("application");
            _applicationId = application.ApplicationID;
            _progress = new Meter(0, 0, application.ResearchCost);
        }
    }

    /// <summary>
    /// Represents a civilization's research pool.
    /// </summary>
    [Serializable]
    public class ResearchPool : INotifyPropertyChanged
    {
        private readonly int _ownerId;
        private readonly int[] _techLevels;
        private readonly DistributionGroup<int> _distributions;
        private readonly ResearchPoolValueCollection _values;
        private readonly ResearchBonusCollection _bonuses;
        private readonly List<ResearchProject>[] _queue;

        private int _cumulativePoints;

        /// <summary>
        /// Gets the owner of this <see cref="ResearchPool"/>.
        /// </summary>
        /// <value>The owner.</value>
        public Civilization Owner
        {
            get { return GameContext.Current.Civilizations[_ownerId]; }
        }

        /// <summary>
        /// Gets the cumulative number of research points generated by the owner.
        /// </summary>
        /// <value>The number cumulative research points generated.</value>
        public int CumulativePoints
        {
            get { return _cumulativePoints; }
        }

        /// <summary>
        /// Gets the distribution of research points between the various fields.
        /// </summary>
        /// <value>The distribution of points between fields.</value>
        public DistributionGroup<int> Distributions
        {
            get { return _distributions; }
        }

        /// <summary>
        /// Gets the research points being allocated to each field for the current turn
        /// </summary>
        /// <value>The current research point allocations.</value>
        public ResearchPoolValueCollection Values
        {
            get { return _values; }
        }

        /// <summary>
        /// Gets the current bonuses available for each research category.
        /// </summary>
        /// <value>The bonuses for each category.</value>
        public ResearchBonusCollection Bonuses
        {
            get { return _bonuses; }
        }

        /// <summary>
        /// Determines whether the specified application has been researched.
        /// </summary>
        /// <param name="application">The application.</param>
        /// <returns>
        /// <c>true</c> if the specified application has been researched; otherwise, <c>false</c>.
        /// </returns>
        public bool IsResearched(ResearchApplication application)
        {
            foreach (ResearchField field in GameContext.Current.ResearchMatrix.Fields)
            {
                if (field.Applications.Contains(application))
                {
                    foreach (ResearchProject project in _queue[field.FieldID])
                    {
                        if (project.Application == application)
                            return false;
                    }
                    return true;
                }
            }
            return false;
        }

        /// <summary>
        /// Determines whether the specified application is currently being researched.
        /// </summary>
        /// <param name="application">The application.</param>
        /// <returns>
        /// <c>true</c> if the application is being researched; otherwise, <c>false</c>.
        /// </returns>
        public bool IsResearching(ResearchApplication application)
        {
            foreach (ResearchField field in GameContext.Current.ResearchMatrix.Fields)
            {
                if (field.Applications.Contains(application))
                {
                    ResearchProject project = GetCurrentProject(field);
                    return ((project != null) && (project.Application == application));
                }
            }
            return false;
        }

        /// <summary>
        /// Gets the current tech level of the specified field.
        /// </summary>
        /// <param name="field">The field.</param>
        /// <returns>The tech level.</returns>
        public int GetTechLevel(ResearchField field)
        {
            if (field == null)
                throw new ArgumentNullException("field");
            return GetTechLevel(field.FieldID);
        }

        /// <summary>
        /// Gets the current tech level of the specified tech category.
        /// </summary>
        /// <param name="category">The tech category.</param>
        /// <returns>The tech level.</returns>
        public int GetTechLevel(TechCategory category)
        {
            var levels = new List<int>();
            foreach (var field in GameContext.Current.ResearchMatrix.Fields.Where(o => o.TechCategory == category))
            {
                levels.Add(GetTechLevel(field));
            }
            if (levels.Count == 0)
                return 0;
            return Algorithms.Minimum(levels);
        }

        /// <summary>
        /// Gets the current tech level of the specified field.
        /// </summary>
        /// <param name="fieldId">The field ID.</param>
        /// <returns>The tech level.</returns>
        public int GetTechLevel(int fieldId)
        {
            return _techLevels[fieldId];
        }


        /// <summary>
        /// Gets the next application to be researched in the specified field.
        /// </summary>
        /// <param name="field">The field.</param>
        /// <returns>The next application.</returns>
        public ResearchApplication GetNextApplication(ResearchField field)
        {
            if (field == null)
                throw new ArgumentNullException("field");
            if (_queue[field.FieldID].Count == 0)
                return null;
            return _queue[field.FieldID][0].Application;
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="ResearchPool"/> class.
        /// </summary>
        /// <param name="owner">The owner.</param>
        /// <param name="matrix">The research matrix.</param>
        public ResearchPool(Civilization owner, ResearchMatrix matrix)
        {
            if (owner == null)
                throw new ArgumentNullException("owner");
            if (matrix == null)
                throw new ArgumentNullException("matrix");
            
            var fieldIds = new List<int>();

            _ownerId = owner.CivID;
            _values = new ResearchPoolValueCollection();
            _bonuses = new ResearchBonusCollection(owner);
            _techLevels = new int[matrix.Fields.Count];
            _queue = new List<ResearchProject>[matrix.Fields.Count];

            var startingTechLevelsTable = GameContext.Current.Tables.GameOptionTables["StartingTechLevels"];
            var startingTechLevel = GameContext.Current.Options.StartingTechLevel;

            Dictionary<TechCategory, int> initialFieldLevelValues = null;

            var ownerIsEmpire = owner.IsEmpire;
            if (ownerIsEmpire)
            {
                initialFieldLevelValues = EnumHelper.GetValues<TechCategory>().ToDictionary(
                    techCategory => techCategory,
                    techCategory =>
                    Number.ParseInt32(startingTechLevelsTable[startingTechLevel.ToString()][techCategory.ToString()]));
            }

            foreach (var field in matrix.Fields)
            {
                fieldIds.Add(field.FieldID);
                _techLevels[field.FieldID] = ownerIsEmpire ? initialFieldLevelValues[field.TechCategory] : 1;
                _queue[field.FieldID] = new List<ResearchProject>();
                foreach (var application in field.Applications)
                {
                    if (application.Level > GetTechLevel(field))
                        _queue[field.FieldID].Add(new ResearchProject(application));
                    else
                        _cumulativePoints += application.ResearchCost;
                }
            }

            _distributions = new DistributionGroup<int>(fieldIds);
            _distributions.DistributeEvenly();
        }

        /// <summary>
        /// Gets the project currently being researched in the specified field.
        /// </summary>
        /// <param name="field">The field.</param>
        /// <returns>The current project.</returns>
        public ResearchProject GetCurrentProject(ResearchField field)
        {
            if (field == null)
                throw new ArgumentNullException("field");
            return GetCurrentProject(field.FieldID);
        }

        /// <summary>
        /// Gets the project currently being researched in the specified field.
        /// </summary>
        /// <param name="fieldId">The field ID.</param>
        /// <returns>The current project.</returns>
        public ResearchProject GetCurrentProject(int fieldId)
        {
            if (_queue[fieldId].Count == 0)
                return null;
            return _queue[fieldId][0];
        }

        /// <summary>
        /// Applies the specified number of research points to the currently active projects.
        /// </summary>
        /// <param name="researchPoints">The number of research points.</param>
        public void UpdateResearch(int researchPoints)
        {
            if (researchPoints < 0)
                researchPoints = 0;
            _distributions.TotalValue = researchPoints;
            _cumulativePoints += researchPoints;
            OnPropertyChanged("CumulativePoints");
            foreach (ResearchField field in GameContext.Current.ResearchMatrix.Fields)
            {
                int fieldPoints = _distributions.Values[field.FieldID];
                fieldPoints += (int)(_bonuses[field.TechCategory] * fieldPoints);
                for (int i = 0; i < _queue[field.FieldID].Count; i++)
                {
                    if (_queue[field.FieldID][i].IsFinished)
                    {
                        FinishProject(field.FieldID, i--);
                        continue;
                    }
                    fieldPoints -= _queue[field.FieldID][i].Progress.AdjustCurrent(fieldPoints);
                    if (_queue[field.FieldID][i].IsFinished)
                    {
                        FinishProject(field.FieldID, i--);
                        continue;
                    }
                    if (fieldPoints <= 0)
                        break;
                }
            }
        }

        /// <summary>
        /// Finishes the project located at the specified index in the queue for a given field.
        /// </summary>
        /// <param name="fieldId">The field id.</param>
        /// <param name="queueIndex">The index in the queue.</param>
        private void FinishProject(int fieldId, int queueIndex)
        {
            var finishedApp = _queue[fieldId][queueIndex].Application;
            var civManager = GameContext.Current.CivilizationManagers[Owner];
            var designsBefore = TechTreeHelper.GetDesignsForCurrentTechLevels(Owner);

            _queue[fieldId].RemoveAt(queueIndex);
            UpdateTechLevels();

            var designsAfter = TechTreeHelper.GetDesignsForCurrentTechLevels(Owner);
            var newDesigns = new List<TechObjectDesign>(
                Algorithms.SetDifference(designsAfter, designsBefore));
            
            if (civManager != null)
                civManager.SitRepEntries.Add(new ResearchCompleteSitRepEntry(Owner, finishedApp, newDesigns));
        }

        /// <summary>
        /// Updates the current tech levels based on new research completed.
        /// </summary>
        protected void UpdateTechLevels()
        {
            foreach (ResearchField field in GameContext.Current.ResearchMatrix.Fields)
            {
                int nextTechLevel = _techLevels[field.FieldID];

                if (GetCurrentProject(field) != null)
                    nextTechLevel = GetCurrentProject(field).Application.Level;

                if (nextTechLevel > _techLevels[field.FieldID])
                    _techLevels[field.FieldID] = nextTechLevel - 1;
            }
        }

        internal void RefreshBonuses()
        {
            OnPropertyChanged("Bonuses");
        }

        #region INotifyPropertyChanged Members
        /// <summary>
        /// Occurs when a property value changes.
        /// </summary>
        [field: NonSerialized]
        public event PropertyChangedEventHandler PropertyChanged;

        /// <summary>
        /// Raises the <see cref="PropertyChanged"/> event.
        /// </summary>
        /// <param name="propertyName">Name of the property that changed.</param>
        protected void OnPropertyChanged(string propertyName)
        {
            if (PropertyChanged != null)
                PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
        }
        #endregion
    }

    /// <summary>
    /// A collection of <see cref="Meter"/>s indexed by <see cref="TechCategory"/>.
    /// </summary>
    [Serializable]
    public class ResearchPoolValueCollection 
        : EnumKeyedValueList<TechCategory, Meter>,
          ICloneable
    {
        #region ICloneable Members
        object ICloneable.Clone()
        {
            return Clone();
        }

        public ResearchPoolValueCollection Clone()
        {
            var clone = new ResearchPoolValueCollection();
            for (var i = 0; i < this.Values.Length; i++)
                clone.Values[i] = this.Values[i].Clone();
            return clone;
        }
        #endregion
    }

    /// <summary>
    /// A collection of percentage-based research bonuses indexed by <see cref="TechCategory"/>
    /// and research field ID.
    /// </summary>
    [Serializable]
    public class ResearchBonusCollection
    {
        private readonly int _ownerId;

        /// <summary>
        /// Gets or sets the percentage-based bonus for the specified field.
        /// </summary>
        /// <value>The bonus.</value>
        public Percentage this[TechCategory field]
        {
            get
            {
                var civManager = GameContext.Current.CivilizationManagers[_ownerId];
                switch (field)
                {
                    case TechCategory.BioTech:
                        return civManager.GlobalBonuses
                            .Where(o => o.BonusType == BonusType.PercentBioTechResearch)
                            .Sum(o => 0.01f * o.Amount);
                    case TechCategory.Computers:
                        return civManager.GlobalBonuses
                            .Where(o => o.BonusType == BonusType.PercentComputerResearch)
                            .Sum(o => 0.01f * o.Amount);
                    case TechCategory.Construction:
                        return civManager.GlobalBonuses
                            .Where(o => o.BonusType == BonusType.PercentConstructionResearch)
                            .Sum(o => 0.01f * o.Amount);
                    case TechCategory.Energy:
                        return civManager.GlobalBonuses
                            .Where(o => o.BonusType == BonusType.PercentEnergyResearch)
                            .Sum(o => 0.01f * o.Amount);
                    case TechCategory.Propulsion:
                        return civManager.GlobalBonuses
                            .Where(o => o.BonusType == BonusType.PercentPropulsionResearch)
                            .Sum(o => 0.01f * o.Amount);
                    case TechCategory.Weapons:
                        return civManager.GlobalBonuses
                            .Where(o => o.BonusType == BonusType.PercentWeaponsResearch)
                            .Sum(o => 0.01f * o.Amount);
                }
                return 0.0f;
            }
        }

        /// <summary>
        /// Gets or sets the percentage-based bonus for the specified field ID.
        /// </summary>
        /// <value>The bonus.</value>
        public Percentage this[int fieldId]
        {
            get { return this[GameContext.Current.ResearchMatrix.Fields[fieldId].TechCategory]; }
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="ResearchBonusCollection"/> class.
        /// </summary>
        public ResearchBonusCollection([NotNull] Civilization owner)
        {
            if (owner == null)
                throw new ArgumentNullException("owner");
            _ownerId = owner.CivID;
        }
    }
}
